<html lang="zh-CN"><head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Jupyter Notebook 转 Markdown</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#165DFF',
            secondary: '#36CFC9',
            accent: '#FF7D00',
            neutral: '#F5F7FA',
            'neutral-dark': '#1D2129',
          },
          fontFamily: {
            inter: ['Inter', 'system-ui', 'sans-serif'],
          },
        },
      }
    }
  </script>
  
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .transition-all-300 {
        transition: all 0.3s ease;
      }
      .shadow-hover {
        transition: box-shadow 0.3s ease;
      }
      .shadow-hover:hover {
        box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
      }
      .bg-gradient-blue {
        background: linear-gradient(135deg, #165DFF 0%, #0E42CC 100%);
      }
    }
  </style>
</head>
<body class="font-inter bg-gray-50 text-neutral-dark min-h-screen flex flex-col">
  <!-- 顶部导航 -->
  <header class="bg-white shadow-sm sticky top-0 z-50">
    <div class="container mx-auto px-4 py-4 flex items-center justify-between">
      <div class="flex items-center space-x-2">
        <i class="fa fa-book text-primary text-2xl"></i>
        <h1 class="text-xl font-bold text-primary">Jupyter 转换工具</h1>
      </div>
      <button class="md:hidden text-gray-600 focus:outline-none" id="mobile-menu-button">
        <i class="fa fa-bars text-xl"></i>
      </button>
    </div>
  </header>

  <!-- 主要内容 -->
  <main class="flex-grow container mx-auto px-4 py-8">
    <section class="max-w-3xl mx-auto bg-white rounded-xl shadow-md p-6 md:p-8 mb-8">
      <h2 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-neutral-dark mb-6 text-center">
        Jupyter Notebook 转 Markdown
      </h2>
      
      <div class="space-y-8">
        <!-- 文件上传区域 -->
        <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary transition-all-300 cursor-pointer bg-neutral" id="drop-area">
          <i class="fa fa-cloud-upload text-5xl text-gray-400 mb-4"></i>
          <h3 class="text-xl font-semibold text-gray-700 mb-2">拖放文件到此处上传</h3>
          <p class="text-gray-500 mb-6">或</p>
          <label for="file-upload" class="inline-block bg-primary hover:bg-primary/90 text-white font-medium py-3 px-6 rounded-lg cursor-pointer transition-all-300 shadow-md hover:shadow-lg">
            <i class="fa fa-file-text-o mr-2"></i> 选择 .ipynb 文件
          </label>
          <input id="file-upload" type="file" accept=".ipynb" class="hidden">
          <p class="text-gray-400 mt-4 text-sm">支持的格式: .ipynb</p>
        </div>
        
        <!-- URL输入区域 -->
        <div class="space-y-4">
          <h3 class="text-lg font-semibold text-gray-700">或从URL导入</h3>
          <div class="flex flex-col sm:flex-row gap-3">
            <input type="text" id="url-input" placeholder="https://example.com/your-notebook.ipynb" class="flex-grow px-4 py-3 rounded-lg border border-gray-300 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all-300">
            <button id="url-btn" class="bg-accent hover:bg-accent/90 text-white font-medium py-3 px-6 rounded-lg transition-all-300 shadow-md hover:shadow-lg">
              <i class="fa fa-link mr-2"></i> 加载
            </button>
          </div>
        </div>
        
        <!-- 进度指示器 -->
        <div id="progress-container" class="hidden">
          <div class="flex items-center justify-between mb-2">
            <span class="text-gray-700 font-medium">转换进度</span>
            <span id="progress-percentage" class="text-primary font-medium">0%</span>
          </div>
          <div class="h-2 bg-gray-200 rounded-full overflow-hidden">
            <div id="progress-bar" class="h-full bg-primary rounded-full" style="width: 0%"></div>
          </div>
        </div>
        
        <!-- 状态信息 -->
        <div id="status-message" class="hidden p-4 rounded-lg text-center">
          <i id="status-icon" class="fa fa-spinner fa-spin text-xl mb-2"></i>
          <p id="status-text" class="text-gray-700"></p>
        </div>
        
        <!-- 下载按钮 -->
        <div id="download-container" class="hidden text-center">
          <button id="download-btn" class="bg-secondary hover:bg-secondary/90 text-white font-medium py-3 px-8 rounded-lg transition-all-300 shadow-md hover:shadow-lg flex items-center mx-auto">
            <i class="fa fa-download mr-2"></i> 下载 Markdown 文件
          </button>
          <p id="filename-display" class="text-gray-500 mt-2"></p>
        </div>
      </div>
    </section>
    
    <!-- 功能说明 -->
    <section class="max-w-3xl mx-auto bg-white rounded-xl shadow-md p-6 md:p-8">
      <h3 class="text-xl font-bold text-neutral-dark mb-4">功能特点</h3>
      <ul class="space-y-4">
        <li class="flex items-start">
          <i class="fa fa-check-circle text-primary mt-1 mr-3"></i>
          <span>完整保留原文件中的 Markdown 内容、代码和输出结果</span>
        </li>
        <li class="flex items-start">
          <i class="fa fa-check-circle text-primary mt-1 mr-3"></i>
          <span>自动将图片转换为 Base64 编码嵌入 Markdown，无需额外附件</span>
        </li>
        <li class="flex items-start">
          <i class="fa fa-check-circle text-primary mt-1 mr-3"></i>
          <span>保留原始文件名，便于组织和管理文档</span>
        </li>
        <li class="flex items-start">
          <i class="fa fa-check-circle text-primary mt-1 mr-3"></i>
          <span>完全在浏览器中运行，无需上传文件到服务器，保护隐私</span>
        </li>
        <li class="flex items-start">
          <i class="fa fa-check-circle text-primary mt-1 mr-3"></i>
          <span>响应式设计，在移动设备和桌面设备上均可流畅使用</span>
        </li>
      </ul>
    </section>
  </main>

  <!-- 页脚 -->
  <footer class="bg-neutral-dark text-white py-8 mt-12">
    <div class="container mx-auto px-4">
      <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
        <div>
          <h3 class="text-lg font-bold mb-4">Jupyter 转换工具</h3>
          <p class="text-gray-400">一个纯前端工具，帮助你轻松将 Jupyter Notebook 转换为 Markdown 文件。</p>
        </div>
      </div>
      <div class="border-t border-gray-700 mt-8 pt-6 text-center text-gray-500 text-sm">
        <p>© 2023 Jupyter 转换工具 | 纯前端实现，无需后端支持</p>
      </div>
    </div>
  </footer>

  <!-- 模态框 -->
  <div id="modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
    <div class="bg-white rounded-xl p-6 max-w-md w-full mx-4">
      <div class="text-center mb-4">
        <i id="modal-icon" class="fa fa-info-circle text-4xl text-primary mb-2"></i>
        <h3 id="modal-title" class="text-xl font-bold text-neutral-dark">提示</h3>
      </div>
      <p id="modal-message" class="text-gray-700 mb-6 text-center"></p>
      <div class="text-center">
        <button id="modal-close" class="bg-primary hover:bg-primary/90 text-white font-medium py-2 px-6 rounded-lg transition-all-300">
          确定
        </button>
      </div>
    </div>
  </div>

  <script>
    // 全局变量
    let currentFilename = 'notebook-converted.md';
    let notebookContent = null;
    let mdContent = null;

    // DOM 元素
    const fileInput = document.getElementById('file-upload');
    const dropArea = document.getElementById('drop-area');
    const urlInput = document.getElementById('url-input');
    const urlBtn = document.getElementById('url-btn');
    const progressContainer = document.getElementById('progress-container');
    const progressBar = document.getElementById('progress-bar');
    const progressPercentage = document.getElementById('progress-percentage');
    const statusMessage = document.getElementById('status-message');
    const statusIcon = document.getElementById('status-icon');
    const statusText = document.getElementById('status-text');
    const downloadContainer = document.getElementById('download-container');
    const downloadBtn = document.getElementById('download-btn');
    const filenameDisplay = document.getElementById('filename-display');
    const mobileMenuButton = document.getElementById('mobile-menu-button');
    const mobileMenu = document.getElementById('mobile-menu');
    const modal = document.getElementById('modal');
    const modalIcon = document.getElementById('modal-icon');
    const modalTitle = document.getElementById('modal-title');
    const modalMessage = document.getElementById('modal-message');
    const modalClose = document.getElementById('modal-close');

    // 初始化事件监听
    function initEventListeners() {
      // 文件上传
      fileInput.addEventListener('change', handleFileUpload);
      
      // 拖放功能
      ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false);
      });
      
      function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
      }
      
      ['dragenter', 'dragover'].forEach(eventName => {
        dropArea.addEventListener(eventName, highlight, false);
      });
      
      ['dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, unhighlight, false);
      });
      
      function highlight() {
        dropArea.classList.add('border-primary');
        dropArea.classList.add('bg-primary/5');
      }
      
      function unhighlight() {
        dropArea.classList.remove('border-primary');
        dropArea.classList.remove('bg-primary/5');
      }
      
      dropArea.addEventListener('drop', handleDrop, false);
      
      function handleDrop(e) {
        const dt = e.dataTransfer;
        const files = dt.files;
        
        if (files.length > 0) {
          processFile(files[0]);
        }
      }
      
      // URL 导入
      urlBtn.addEventListener('click', handleUrlImport);
      
      // 下载按钮
      downloadBtn.addEventListener('click', downloadMarkdown);
      
      // 移动端菜单
      mobileMenuButton.addEventListener('click', toggleMobileMenu);
      
      // 模态框关闭
      modalClose.addEventListener('click', closeModal);
    }

    // 处理文件上传
    function handleFileUpload(e) {
      const file = e.target.files[0];
      if (file) {
        processFile(file);
      }
    }

    // 处理 URL 导入
    function handleUrlImport() {
      const url = urlInput.value.trim();
      if (!url) {
        showModal('错误', '请输入有效的 URL', 'error');
        return;
      }
      
      // 验证 URL 是否为 .ipynb 文件
      if (!url.endsWith('.ipynb')) {
        showModal('错误', '请输入有效的 .ipynb 文件 URL', 'error');
        return;
      }
      
      // 提取文件名
      currentFilename = url.split('/').pop();
      if (!currentFilename.endsWith('.ipynb')) {
        currentFilename = 'notebook-converted.md';
      } else {
        currentFilename = currentFilename.replace('.ipynb', '.md');
      }
      
      showProgress('正在从 URL 加载文件...');
      setProgress(10);
      
      fetch(url)
        .then(response => {
          if (!response.ok) {
            throw new Error(`HTTP 错误: ${response.status}`);
          }
          setProgress(30);
          return response.json();
        })
        .then(nb => {
          setProgress(60);
          notebookContent = nb;
          setProgress(80);
          convertToMarkdown();
          setProgress(100);
          showDownload();
        })
        .catch(error => {
          console.error('加载 URL 失败:', error);
          showModal('错误', `加载 URL 失败: ${error.message}`, 'error');
          hideProgress();
        });
    }

    // 处理文件
    function processFile(file) {
      if (!file.name.endsWith('.ipynb')) {
        showModal('错误', '请选择有效的 .ipynb 文件', 'error');
        return;
      }
      
      // 保存原始文件名
      currentFilename = file.name.replace('.ipynb', '.md');
      
      showProgress('正在加载文件...');
      setProgress(10);
      
      const reader = new FileReader();
      
      reader.onload = function(e) {
        try {
          setProgress(40);
          const nb = JSON.parse(e.target.result);
          notebookContent = nb;
          setProgress(60);
          convertToMarkdown();
          setProgress(90);
          showDownload();
          setProgress(100);
        } catch (error) {
          console.error('解析文件失败:', error);
          showModal('错误', `解析文件失败: ${error.message}`, 'error');
          hideProgress();
        }
      };
      
      reader.onerror = function() {
        showModal('错误', '读取文件失败', 'error');
        hideProgress();
      };
      
      reader.readAsText(file);
    }

    // 转换为 Markdown
    function convertToMarkdown() {
      if (!notebookContent) return;
      
      let markdown = '';
      const cells = notebookContent.cells || [];
      const totalCells = cells.length;
      
      // 遍历所有单元格
      cells.forEach((cell, index) => {
        // 更新进度
        setProgress(60 + Math.floor((index / totalCells) * 30));
        
        if (cell.cell_type === 'markdown') {
          // 直接添加 Markdown 内容
          markdown += cell.source + '\n\n';
        } else if (cell.cell_type === 'code') {
          // 代码块
          markdown += '```python\n';
          markdown += cell.source + '\n';
          markdown += '```\n\n';
          
          // 输出结果
          if (cell.outputs && cell.outputs.length > 0) {
            markdown += '**输出结果**\n\n';
            
            cell.outputs.forEach(output => {
              if (output.output_type === 'error') {
                // 错误输出
                markdown += '```error\n';
                markdown += `错误: ${output.ename} - ${output.evalue}\n`;
                if (output.traceback) {
                  markdown += output.traceback.join('\n') + '\n';
                }
                markdown += '```\n\n';
              } else if (output.output_type === 'display_data' || 
                         output.output_type === 'execute_result' ||
                         output.output_type === 'stream') {
                // 文本输出
                if (output.text || output.data['text/plain']) {
                  markdown += '```console\n';
                  markdown += (output.text || output.data['text/plain']) + '\n';
                  markdown += '```\n\n';
                }
                
                // HTML 输出
                if (output.data && output.data['text/html']) {
                  markdown += output.data['text/html'] + '\n\n';
                }
                
                // 图片输出 (PNG)
                if (output.data && output.data['image/png']) {
                  const imgBase64 = output.data['image/png'];
                  markdown += `<img src="data:image/png;base64,${imgBase64}" alt="输出图片" style="max-width:100%;">\n\n`;
                }
                
                // 图片输出 (JPEG)
                if (output.data && output.data['image/jpeg']) {
                  const imgBase64 = output.data['image/jpeg'];
                  markdown += `<img src="data:image/jpeg;base64,${imgBase64}" alt="输出图片" style="max-width:100%;">\n\n`;
                }
                
                // 其他格式 (如 SVG)
                if (output.data && output.data['image/svg+xml']) {
                  markdown += '<div class="svg-container" style="max-width:100%; overflow-x:auto;">\n';
                  markdown += output.data['image/svg+xml'] + '\n';
                  markdown += '</div>\n\n';
                }
              }
            });
          }
        }
      });
      
      mdContent = markdown;
    }

    // 下载 Markdown 文件
    function downloadMarkdown() {
      if (!mdContent) return;
      
      const blob = new Blob([mdContent], { type: 'text/markdown;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      
      const a = document.createElement('a');
      a.href = url;
      a.download = currentFilename;
      
      // 触发下载
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      
      // 释放 URL 对象
      setTimeout(() => URL.revokeObjectURL(url), 100);
      
      // 显示成功提示
      showModal('成功', `已成功下载文件: ${currentFilename}`, 'success');
    }

    // 显示进度
    function showProgress(message) {
      progressContainer.classList.remove('hidden');
      statusMessage.classList.remove('hidden');
      downloadContainer.classList.add('hidden');
      
      statusIcon.className = 'fa fa-spinner fa-spin text-xl mb-2 text-primary';
      statusText.textContent = message;
    }

    // 隐藏进度
    function hideProgress() {
      progressContainer.classList.add('hidden');
      statusMessage.classList.add('hidden');
    }

    // 设置进度百分比
    function setProgress(percentage) {
      progressBar.style.width = `${percentage}%`;
      progressPercentage.textContent = `${percentage}%`;
    }

    // 显示下载按钮
    function showDownload() {
      progressContainer.classList.add('hidden');
      statusMessage.classList.add('hidden');
      downloadContainer.classList.remove('hidden');
      
      filenameDisplay.textContent = `文件名: ${currentFilename}`;
    }

    // 切换移动端菜单
    function toggleMobileMenu() {
      mobileMenu.classList.toggle('hidden');
    }

    // 显示模态框
    function showModal(title, message, type = 'info') {
      modalTitle.textContent = title;
      modalMessage.textContent = message;
      
      // 设置图标
      if (type === 'success') {
        modalIcon.className = 'fa fa-check-circle text-4xl text-green-500 mb-2';
      } else if (type === 'error') {
        modalIcon.className = 'fa fa-exclamation-circle text-4xl text-red-500 mb-2';
      } else {
        modalIcon.className = 'fa fa-info-circle text-4xl text-primary mb-2';
      }
      
      modal.classList.remove('hidden');
    }

    // 关闭模态框
    function closeModal() {
      modal.classList.add('hidden');
    }

    // 页面加载完成后初始化
    document.addEventListener('DOMContentLoaded', function() {
      initEventListeners();
    });
  </script>


</body></html>